Profile picture

[Linux] 리눅스 서버 부하의 주범, Load Average! 원인 분석부터 해결까지!

JaehyoJJAng2025년 01월 20일

개요

시스템 엔지니어로 일하다 보면, 어느 날 갑자기 서버가 느려지고 있다는 연락을 받고는 합니다.


이 때 가장 먼저 uptime이나 top 명령어로 확인하는 지표,

바로 Load Average 가 하늘 높은 줄 모르고 치솟고 있는 상황이죠. 😱


Load Average는 CPU 사용률과는 다른 개념으로, 서버가 얼마나 바쁜가? 를 알려주는 중요한 지표입니다.


정확히는 '실행 대기 중(Runnable)'이거나 '대기 상태(Uninterruptible Sleep)'에 있는 프로세스들의 평균 개수를 의미합니다.


이번 게시글에서는 실무에서 Load Average가 급격하게 올라가는 대표적인 원인들과,

시스템엔지니어로서 어떻게 체계적으로 점검하고 해결해야 하는지, 그리고 가상 시나리오까지 부여하여 해결하는 과정까지 포함할 예정입니다.


🕵️‍♂️ Load Average, 왜 갑자기 치솟는 걸까?

Load Average가 높다고 해서 무조건 CPU가 100% 일하고 있다는 의미는 아니에요!

오히려 다른 곳에 병목이 있을 확률이 꽤 있습니다.


자주 발생하는 원인은 크게 세 가지로 나눌 수 있을 것 같습니다.


1. CPU 자원의 부족 (cpu-bound)

가장 직관적인 원인이죠.

프로세스가 CPU를 너무 많이 사용해서 다른 프로세스들이 자기 차례를 기다리며 길게 줄을 서는 상황인겁니다.


  • 잘못된 애플리케이션 코드: 무한 루프에 빠진 코드, 비효율적인 알고리즘을 사용하는 로직 등
  • 과도한 데이터 처리: 대용량 데이터 압축, 암호화/복호화, 이미지/비디오 인코딩 등
  • 컴파일 및 빌드 작업: 대규모 소스 코드를 컴파일하는 경우

2. I/O 작업 대기 (I/O bound)

CPU는 할 일이 있는데, 디스크나 네트워크의 응답이 느려서 발이 묶인 상태입니다.

프로세스는 "대기 상태"에 빠지고, 이게 쌓여서 Load Average를 올립니다.


실무에서 가장 흔하게 발생하는 원인 중 하나라고 생각합니다.


  • 느린 디스크: HDD에서 수많은 작은 파일을 읽고 쓰거나, 대용량의 DB 쿼리가 디스크를 괴롭히는 경우
  • 메모리 부족으로 인한 스왑 발생: RAM이 부족해지면 운영체제는 잘 안 쓰는 메모리 데이터를 디스크(Swap 공간)에 내려놓습니다. 다시 이 데이터가 필요해지면 디스크에서 읽어와야 하는데, 이 과정에서 심각한 I/O 병목이 발생합니다.
  • 네트워크 지연: 외부 API를 호출하거나 NFS 같은 네트워크 파일 시스템을 사용하는데 응답이 느린 경우

3. 너무 많은 프로세스/스레드

프로세스나 스레드 자체가 너무 많이 생성되어서 CPU가 이들을 관리(Context Switching)하는 자원을 소모하는 경우입니다.


  • 프로세스 폭탄 (Fork Bomb): 악의적이거나 잘못 작성된 스크립트가 자기 자신을 무한정 복제하는 경우
  • 웹 서버 설정 오류: Apache나 Nginx의 Worker 프로세스/스레드 설정을 너무 높게 잡아서 감당하지 못하는 경우
  • 순간적인 트래픽 폭증: 갑작스러운 이벤트나 DDoS 공격으로 인해 수많은 요청이 한꺼번에 몰려드는 경우

💻 단계별 트러블슈팅 가이드

Load Average가 높다는 알림을 받았다면?

당황하지 말고 아래 순서대로 시스템을 점검해봅시다.


1. 현상 파악 및 기본 정보 수집

가장 먼저 현재 시스템의 전반적인 상태를 수집해 봐야겠죠?


  • uptime: 현재 시간, 서버 가동 시간, 접속자 수, 그리고 가장 중요한 Load Average(1분, 5분, 15분) 를 확인합니다. 이 떄 1분 평균이 15분 평균보다 월등히 높다면 부하가 최근에 시작된거겠죠?
  • top 또는 htop: 실시간으로 프로세스 상태를 보여주는 도구죠? CPU, 메모리 사용량이 높은 프로세스를 즉시 확인할 수 있습니다. 특히 이 떄 %wa (I/O Wait) 수치를 주목하세요. 이 수치가 높다면 I/O 병목이 발생한 걸수도 있습니다.
  • dmesg | tail: 커널 로그를 확인해서 OOM(Out of Memory) Killer가 작동했거나, 디스크 에러등 하드웨어 이상이 없는지 점검합시다.

2. 병목 지점 추적

1단계에서 얻은 정보를 기반으로 부하의 원인이 CPU인지, I/O인지 범위를 좁혀나갑시다.


2-1. CPU 병목이 의심될 때

  • top에서 CPU 사용량 (%us, %sy)이 비정상적으로 높은 프로세스를 찾습니다.
    • %us (user space): 우리가 일반적으로 실행하는 대부분의 프로그램, 예를 들어 웹 서버(Nginx), 데이터베이스(MySQL), 그리고 직접 작성한 파이썬 스크립트나 컴파일된 C 프로그램 등이 여기에 해당합니다. 즉, 커널의 도움이 없이 순수하게 애플리케이션 로직을 처리하는 데 사용된 CPU 시간을 의미합니다.
    • %sy (system / kernel space): 사용자 프로그램은 파일 읽기/쓰기, 네트워크 통신, 메모리 할당 등 하드웨어 자원에 직접 접근할 수 없습니다. 대신 커널에게 "이것 좀 해줘"라고 요청(System Call)하는데, 이 요청을 처리하는 데 사용된 CPU 시간이 바로 %sy입니다.
  • ps -ef --sort=%cpu | head: CPU 사용량 순으로 정렬하여 보여줍니다.

2-2. I/O 병목이 의심될 때 (%wa 가 높을 때)

  • iotop: 어떤 프로세스가 디스크 I/O를 유발하는지 실시간으로 보여주는 도구입니다.
  • iostat -x 1: 1초 간격으로 디스크별 I/O 상태(%util, await 등)를 자세히 보여줍니다. 특정 디스크의 사용률이 100%에 가깝다면 그 디스크가 병목이겠죠?
  • lsof -p [PID]: 특정 프로세스(PID)가 어떤 파일들을 열고 있는지 확인하여 문제의 원인이 되는 파일을 찾아봅시다.

2-3. 메모리 상태 점검

메모리 부족은 결국 I/O 병목으로 이어지는 경우가 많습니다.

  • free -h: 전체 메모리, 사용 중인 메모리, 그리고 Swap 사용량을 확인합니다. Swap 사용량이 조금이라도 있다면 메모리 부족을 의심해야 합니다.
  • vmstat 1 : 1초 간격으로 메모리, 스왑, I/O 등 전반적인 시스템 상태를 보여줍니다. si(swap-in), so(swap-out) 컬럼에 0이 아닌 값이 계속 찍힌다면 메모리 스와핑이 활발하게 일어나고 있다는 증거입니다.

💡 실력 향상을 위한 가상 시나리오 및 테스트

이론만 아는 것과 직접 경험하여 해결해보는 것은 천지 차이입니다.

개인 테스트 서버나 가상 머신에서 직접 부하 상황을 만들어보고 해결하는 연습을 해봅시다.


💻 시나리오 1: CPU 과부하 (CPU-Bound)

가장 고전적인 부하 유형이죠?

특정 프로세스가 과도한 연산을 수행하며 CPU 자원을 독점하는 상황입니다.


상황 설정

데이터 분석용 배치(Batch) 스크립트를 서버에서 실행했는데,

그 이후로 서버의 전반적인 반응 속도가 매우 느려졌다는 연락을 받았습니다.


증상 재현 코드: cpu_hog.py

아래 파이썬 코드는 단순 곱셈을 무한 반복하여 의도적으로 CPU에 100% 부하를 줍니다.

# cpu_hog.py
print("CPU를 집중적으로 사용하는 작업을 시작합니다...")
print("이 프로세스를 중지하려면 Ctrl+C를 누르거나 kill 명령어를 사용하세요.")

x = 0
while True:
    x = 123456789 * 987654321

트러블슈팅 및 테스트 과정

1. 부하 발생: 테스트 서버에서 위 스크립트 실행해봅시다.

nohup python cpu_hog.py >&/dev/null &

2. 1차 진단 (uptime, top)

uptime을 실행하여 Load Average가 빠르게 상승하는지 확인합시다.


아래 사진은 위 스크립트 실행 후 1분이 지난 시점의 평균 부하값입니다.
image


2분이 지난 시점의 평균 부하값을 조회하니 다음과 같네요.
image
점점 평균 부하가 올라가고 있습니다.


top을 실행하여 CPU 상태 라인을 주목해봅시다.

  • %us: 값이 100%에 가깝게 치솟나요?
  • 반면 %wa 값은 매우 낮게 유지되나요?
  • 프로세스 목록 최상단에 방금 실행한 python 프로세스의 CPU 점유율(%CPU)이 99.9% 이상 나타납니까?

image
예상대로 최상단에서 가장 높은 부하를 받고 있는 프로세스가 cpu_hog.py로 특정되었습니다.


3. 원인 프로세스 특정하기

top에서 이미 확인했지만,

ps -eo pid,ppid,%cpu,%mem,cmd --sort=-%cpu | head 명령으로 CPU를 많이 쓰는 프로세스를 다시 한번 명확히 특정해봅시다.
image


4. 분석 및 해결

  • 분석: Load Average 상승의 원인이 I/O나 메모리가 아닌, 순수하게 cpu_hog.py 스크립트의 과도한 연산 때문임을 알 수 있었습니다.
  • 해결: top에서 확인한 PID를 이용해 해당 프로세스를 종료해줍시다.

💻 시나리오 2: 메모리 부족 및 스왑 (memory-bound)

애플리케이션이 RAM을 모두 소진하여,

디스크 공간(swap)을 메모리처럼 사용하기 시작하면서 시스템 전체가 극심하게 느려지는 상황입니다.


상황 설정

새로운 버전의 애플레키엿ㄴ을 배포한 후 서버가 거의 멈춘 것 처럼 느려졌습니다.

SSH 접속조차 간헐적으로 끊기는 현상이 발생합니다.


증상 재현 코드: memory_eater.py

이 코드는 RAM이 가득찰 때까지 리스트에 계속해서 데이터를 추가하여 메모리 부족 및 스왑을 유발합니다.

import time

data_list: list = []
try:
    while True:
        # 10MB 크기의 문자열을 리스트에 추가
        data_list.append(' ' * 10 * 1024 * 1024)
        time.sleep(1)
except MemoryError:
    while True:
        time.sleep(60)

트러블슈팅 및 테스트 과정

1. 부하 발생: 테스트 서버에서 위 스크립트 실행해봅시다.

nohup python memory_eater.py >&/dev/null &

2. 1차 진단 (free, top)

free -h 명령을 실행하여 Swapused가 0이 아니고 계속 증가하는지 검증해봅시다.

이 때 당연히 Memfree는 거의 0에 가까워지겠죠?


이후 top 명령을 실행하여 Load Average가 높은지 확인해봅시다.

또한, %wa 값이 매우 높게 나타날겁니다. (디스크를 메모리처럼 쓰기 때문에!)


여기서 kswapd0라는 커널 프로세스의 CPU 사용량도 추가적으로 보일 수 있어요.


전체적으로 점검해보면 python 프로세스의 메모리 점유율(%MEM)이 매우 높고, 상태(S)가 D (Uninterruptible sleep)로 자주 보입니다.


3. I/O 상태 확인 (vmstat, iotop)

vmstat 1 명령을 실행하여 swap 컬럼의 si(swai-in), so(swap-out) 값이 계속해서 올라가는지 확인해봅시다.

이는 디스크와 메모리 간의 데이터 교환이 활발하다는 명백한 증거가 됩니다.


또한 iptop 명령을 실행해 python 프로세스나 kswapd0가 높은 디스크 I/O를 유발하는 것을 직접 볼 수 있어요.


4. 분석 및 해결

  • 분석: memory_eater.py가 메모리를 과다하게 사용하여 스왑이 발생헀고, 이로 인한 디스크 I/O 병목이 Load Average 급증의 원인입니다.
  • 해결: kill -9 [PID]로 메모리를 점유한 프로세스를 강제적으로 종료 시켜주세요 (이 때 kill이 안 먹힐 수도 있습니다..!)

💻 시나리오 3: 네트워크 대기 (Network I/O)

우리 서버에는 문제가 없지만,

서버가 의존하는 외부 서비스(API,DB 등)가 느려지면서 그 영향으로 우리 서버의 요청 처리 스레드들이 모두 대기 상태에 빠지는 상황입니다.


상황 설정

사용자 로그인을 처리하는 API 서버의 응답이 갑자기 매우 느려졌습니다.

서버의 CPU나 메모리 사용량은 낮은데도 불구하고 Load Average는 높고 서비스는 거의 마비 상태입니다.


증상 재현 코드: network_wait.py

Flask 웹 서버가 요청을 받으면, 응답이 매우 느린 외부 사이트에 접속을 시도하며 하염없이 기다리는 상황을 연출합니다.

# network_wait.py
from flask import Flask
import requests

app = Flask(__name__)

@app.route('/login')
def login_user():
    print("외부 인증 API 호출 시작...")
    try:
        # 의도적으로 응답이 10초 걸리는 외부 API 호출
        # timeout을 길게 잡아 문제가 발생하도록 유도
        response = requests.get('https://httpbin.org/delay/10', timeout=30)
        print("API 응답 수신 완료.")
        return "Login Success!"
    except requests.exceptions.Timeout:
        print("API 호출 타임아웃.")
        return "Error: Auth service timeout", 504

if __name__ == '__main__':
    # 동시 요청을 처리하도록 threaded=True 설정
    app.run(host='0.0.0.0', port=5000, threaded=True)

트러블슈팅 및 테스트 과정

1. 부하 발생

  • 1. Flaskrequests 라이브러리를 설치합니다. (pip install Flask requests)
  • 2. 스크립트를 실행합니다. (python network_wait.py)
  • 3. 새 터미널에서 ab로 동시 요청을 보냅니다.
ab -n 20 -c 20 http://127.0.0.1:5000/login

2. 1차 진단 (top, netstat)

  • top: Load Average는 오르지만, %us, %sy, %wa가 모두 낮게 나타나는 기묘한 현상을 관찰할 수 있습니다. 시스템 자원은 널널한데 서버는 바쁜 아이러니한 상황이죠. 프로세스 목록의 python 프로세스들은 대부분 S(Sleep) 상태일 것입니다.
  • netstat -anp | grep ESTABLISHED | grep python: python 프로세스가 httpbin.org의 IP(예: 54.164.84.15)와 연결된 ESTABLISHED 상태의 소켓을 많이 가지고 있는 것을 확인합니다.

3. 심층 분석 (strace)

strace -p [PID]로 대기 중인 python 프로세스를 추적해보면, recvfrom이나 poll 같은 네트워크 관련 시스템 콜에서 멈춰있는 걸(blocked) 볼 수 있습니다.


4. 분석 및 해결

  • 분석: 외부 API(httpbin.org/delay/10)의 응답 지연으로 인해 웹 애플리케이션의 Worker 스레드들이 모두 대기 상태에 빠졌고, 새로운 요청을 처리하지 못해 Load Average가 상승했습니다.
  • 해결: Flask 서버를 재시작하여 대기 중인 모든 연결을 끊습니다.
    Tag -

Loading script...